/*
 *		RemoteClient.java
 *
 *		Created on 04 JAN 2005 by Duane Bolick
 *		ver 1.0
 *
 *		This is the class that does everything.  The main method (located in
 *		ClientMain.java) creates a RemoteClient object, and then calls its
 *		go() method, which is where all the magic happens.  This app uses
 *		multiple threads of execution to do its job.
 *
 *
 *
 *							--- Java threading basics ---
 *
 *		In Java, separate threads of execution are created using the Thread
 *		object.  You create a Thread object, give it a job, and then call the
 *		start() method of the thread object.  The job of the thread can be
 *		any object that implements the Runnable interface.  To implement this
 *		interface, you need to write one method, with the following signature:
 *
 *				public void run()
 *
 *		This method is what gets executed by the Thread object when you call
 *		start().  Here's a quick example:
 *
 *				SomeClass job = new SomeClass();
 *				Thread t = new Thread( job );
 *				t.start();
 *
 *		SomeClass must implement Runnable, and it must have a run() method.
 *		Here's what it might look like:
 *
 *				class SomeClass implements Runnable
 *				{
 *					public void run()
 *					{
 *						while( true )
 *						{
 *							System.out.println("I'm running!");
 *						}
 *					}
 *				}
 *
 *		And that's all I have to say about that.
 *
 *
 *
 *
 *							--- RemoteClient basics ---	
 *
 *		So we have our RobotController object, written by the end-user.  The
 *		code in that object tells the robot how to move around and manipulate
 *		its environment, based on the values of its sensors, etc.  This remote
 *		client app maintains a virtual robot object - the CurrentRobotState
 *		object - that holds all the data - sensor values, wheel speeds, etc,
 *		that the real Khepera knows about itself.  The controller code interacts
 *		with this virtual robot.  That is, the sensor values it uses to make 
 *		decisions comes from the CurrentRobotState's sensor array, when it
 *		needs to change the robot's motor speeds, it changes the motor speed
 *		variables in the CurrentRobotState object, when it needs to raise the
 *		arm, it changes the armState variable in the CurrentRobotState object,
 *		etc.
 *
 *		It is therefore critical to the proper operation of this program that
 *		the CurrentRobotState object contains data about the Khepera that is
 *		as current as possible.  In addition, it is very important that, when
 *		changes are made to the CurrentRobotState's motors and gripper arm
 *		variables, that those changes are written to the real Khepera, in the
 *		form of Khepera Serial Language commands, as soon as the changes are
 *		made.
 *
 *		In all, there are 4 jobs that the RemoteClient application needs to do
 *		at the same time while connected to the server.  First, it needs to
 *		listen on the socket for incoming messages from the server, and update
 *		the CurrentRobotState object accordingly (i.e., if it receives new
 *		distance sensor values, it changes the sensor values in the robot state
 *		sensor array).  Second, it needs to write khepera-lenguage commands to
 *		the socket, asking for fresh sensor data every so often (well, quite
 *		frequently, actually...)  Third, it needs to write khepera-language
 *		commands to the socket whenever an effector (i.e., the motors, the
 *		gripper arm, the gripper) is changed by the controller.  Fourth, it
 *		needs to run the controller, itself (that's the RobotController object
 *		written by the end user, of course).
 *		
 *		For each of these 4 jobs, we've written a class that implements the
 *		Runnable interface, and put the code to do each job in the run() method
 *		of each class.  Here are the names of the Runnable classes, and the
 *		name of the thread.
 *
 *		=============		===============================		================
 *		Runnable			Job	Description						Thread Name
 *		=============		===============================		================
 *		Listener			Listens on the socket, updates		listenerThread
 *							CurrentRobotState accordingly 
 *		
 *		SensorUpdater		Sends Khepera-language commands		updaterThread
 *							asking the Khepera for its
 *							sensor data periodically
 *
 *		CommandWriter		Sends Khepera-language commands		commanderThread
 *							whenever the effector variables
 *							in the CurrentRobotState are
 *							altered by the controller
 *
 *		RobotController		Runs the user's controller			controllerThread
 *					
 *
 *		Each of these Runnables has a variable that controls how often it runs
 *		by putting it to sleep for a few milliseconds after each execution
 *		cycle.  Those variables are listenerThreadTimeout, updaterThreadTimeout,
 *		commanderThreadTimeout, and controllerThreadTimeout.  You can change
 *		the values of these timeout variables in the file client.conf.
 *							
 *		The basic procedure (which you see 4 times in the code below) is to
 *		
 *		1) Create the Runnable object
 *
 *		SensorUpdater updater = new SensorUpdater( writer, state, updaterThreadTimeout );
 *
 *
 *		2) Create a Thread using the Runnable object
 *
 *		updaterThread = new Thread( updater );
 *
 *
 *		3) Start the thread
 *
 *		updaterThread.start();
 *
 *	
 *		Simple, right?
 *
 */

package edu.wsu.KheperaSimulator;
import java.io.*;
import java.net.*;


public class RemoteClient
{
	// Networking stuff
	private String ip;
	private int port;
	private Socket socket;
	public BufferedReader reader;
	public PrintWriter writer;


	// Utility stuff
    private RobotControllerLoader loader;
    private String name;
    private String webcamURL;
    private ClientConfiguration config;


	// Runnable objects
 	public Listener listener;
  	public SensorUpdater updater;
  	public CommandWriter commander;
  	private RobotController controller;
  
  
  	// Threads for the runnables
	private Thread controllerThread;
	private Thread listenerThread;
	private Thread updaterThread;
	private Thread commanderThread;
	
	
	// Intervals for the threads
	private long controllerThreadTimeout;
	private long listenerThreadTimeout;
	private long updaterThreadTimeout;
	private long commanderThreadTimeout;


	// Virtual robot object
    private CurrentRobotState state;

   
	// =========================================================================
	// ===                          ============================================
	// ===		Public Methods		============================================
	// ===                          ============================================
	// =========================================================================
    


	public RemoteClient()
	{
		config = new ClientConfiguration();
		
		ip = config.IP;
		port = config.PORT;	
		webcamURL = config.WEBCAM_URL;
		
		controllerThreadTimeout = config.CONTROLLER_TIMEOUT;
		listenerThreadTimeout = config.LISTENER_TIMEOUT;
		updaterThreadTimeout = config.UPDATER_TIMEOUT;
		commanderThreadTimeout = config.COMMANDER_TIMEOUT;
		
		loader = new RobotControllerLoader( config.PATH );
	
		controller = null;
	}

   
   
	public void go()
	{
		printIntro();
								
		while( controller == null )
		{
			name = getControllerName();
			controller = loader.getController(name);
		}
		
				
		connectToServer();
		
		state = new CurrentRobotState();
		
		// Start the listener thread
		listener = new Listener( listenerThreadTimeout, reader, state );
		listenerThread = new Thread( listener );
		listenerThread.start();
		
		waitForTurn();
		
		// Start the sensor update thread		
		updater = new SensorUpdater( updaterThreadTimeout, writer, state );
		updaterThread = new Thread( updater );
		updaterThread.start();
		
		// Start the effector-writing thread
		commander = new CommandWriter( commanderThreadTimeout, writer, state );
		commanderThread = new Thread( commander );
		commanderThread.start();
		
		// Initialize the robot
		state.setArmState( KSGripperStates.ARM_UP );
		state.setGripperState( KSGripperStates.GRIP_OPEN );

		// Start the controller thread
		controller.initialize( name, state, controllerThreadTimeout );
		controllerThread = new Thread( controller );
		controllerThread.start();
		
		
		while( state.sessionStatus == ClientConfiguration.RUNNING )
		{
			try
			{
				Thread.sleep(1000);
			}
			
			catch(Exception e)
			{
			}
		}
			
		cleanup();

		return;
	}
   

		
	
   

   
	// =========================================================================
	// ===                          ============================================
	// ===		Private Methods		============================================
	// ===                          ============================================
	// =========================================================================
    
    
    private void cleanup()
    {
    	if( state.sessionStatus == ClientConfiguration.TIMEOUT )
		{
			System.out.println("Your session time limit is up.");
		}
		
		else if( state.sessionStatus == ClientConfiguration.BROKEN )
		{
			System.out.println("The connection was lost.");
		}
		
		else if( state.sessionStatus == ClientConfiguration.STUCK )
		{
			System.out.println("The Khepera is stuck.");
			System.out.println("Ending run.");
		}
    }
    
    private void printIntro() 
    {
    	System.out.println("WSU Khepera Remote Interface\nPress CTRL-C to exit\n");
    	System.out.println("Webcam URL: " + webcamURL + "\n");
	}
        
    private void waitForTurn()
    {
    	int lastWaitTimeLength = 0;
    	
    	System.out.print("Your turn ");
    	
    	while( state.sessionStatus == ClientConfiguration.WAITING || state.sessionStatus == ClientConfiguration.STUCK ) 
		{	
			// need to wait for a bit here so state can update
			pause(listenerThreadTimeout*4L);
			
			for( int i = 0 ; i < lastWaitTimeLength ; i++ )
			{
				System.out.print("\b");
				System.out.print(" ");
				System.out.print("\b");
			}
			
			System.out.print(state.waitTime);
			lastWaitTimeLength = state.waitTime.length();
		}
		
		System.out.println();
    }
    
    private void connectToServer()
	{
		try
		{
			socket = new Socket(ip, port);
			InputStreamReader iStream  = new InputStreamReader(socket.getInputStream());
			OutputStreamWriter oStream  = new OutputStreamWriter(socket.getOutputStream());;
			
			reader = new BufferedReader(iStream);
			writer = new PrintWriter(oStream);
		}
		
		catch(IOException ex)
		{
			System.out.println("Server Unavailable\n");
			System.exit(0);
		}
	}
   
    
 
	private String getControllerName()
    {
    	BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
		
		String input = null;
		String cName = null;
		String[] controllerNames;
		
        boolean done = false;
        int length;
        
	  	Integer i = new Integer(-9);
	  	
	  	
	  	controllerNames = loader.getControllerNames();
    	length = controllerNames.length - 1;
	  
	  	System.out.println("Please type the number for the Controller you wish to run");
	  
	  	for(int x = 0; x < controllerNames.length; x++)
	  	{
			System.out.println(x+"  "+controllerNames[x]);
	  	}
	  
	  	while(!done)
	  	{
	   		try
	   		{
                input = in.readLine();
		    	try
		    	{
					i = new Integer(input);
				}
		    
		    	catch (NumberFormatException e)
		    	{
                	System.out.println("Input was not a number");
                }
		    
		    	if((i.intValue() < controllerNames.length) && (i.intValue() >= 0))
		    	{
					cName = controllerNames[i.intValue()];
					done = true;
		    	}
		    
		    	else
					System.out.println("Please choose a number between 0 and "+length);
            }
            
            catch (IOException e) 
            {
            	System.out.println("Please choose a number between 0 and "+length);
		    	input = null;
            }
            
    	}//end of while
	  
	  	
	  	return cName;
	}
	  
	  
	private void pause( long timeout )
	{
		try
		{
			Thread.sleep( timeout );
		}
		
		catch( Exception e )
		{
			e.printStackTrace();
		}
	}

  
}
